// javamaps
// Copyright (c) 2010 Lup Gratian
package Rendering.Renderers;
import Rendering.IVisual;
import Rendering.*;
import Rendering.Utils.*;
import Core.*;
import Rendering.Animations.*;
import Rendering.Info.MarkerInfo;
import java.awt.Graphics2D;
import java.io.IOException;
import java.util.*;
import java.awt.image.*;
import java.awt.Color;
import java.awt.Font;
import java.awt.Image;
import java.awt.FontMetrics;
import java.awt.AlphaComposite;
import java.awt.RenderingHints;
import java.util.logging.Level;
import java.util.logging.Logger;

public final class MarkerRenderer implements IRenderer, IAnimatable {
    // Contine informatii asociate cu un marker.
    public class MarkerVisual implements IVisual {
        private Marker marker_;
        private Point position_;

        public MarkerVisual(Marker marker, Point point) {
            marker_ = marker;
            position_ = point;
        }

        public double X() { return position_.X(); }
        public double Y() { return position_.Y(); }
        public Marker Marker() { return marker_; }
        public Point Position() { return position_; }
        public void SetPosition(Point value) { position_ = value; }

        public void Draw(Graphics2D g, View view) {
            // Sunt desenate de clasa principala.
        }

        @Override
        public int hashCode() {
            return marker_.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            MarkerVisual other = (MarkerVisual) obj;
            return marker_.ID().equals(other.Marker().ID());
        }
    }


    // Deseneaza numele unui marker pe layer-ul Overlay
    // (peste toate celelalte layer-e).
    class MarkerOverlay implements IVisual {
        MarkerVisual markerVis_;
        NameAnimation animation_;

        public MarkerOverlay(MarkerVisual visual, NameAnimation animation) {
            markerVis_ = visual;
            animation_ = animation;
        }

        public void Draw(Graphics2D g, View view) {
            double viewX = view.Bounds().Left();
            double viewY = view.Bounds().Top();
            double sx = GetScaleX(view.Zoom());
            double sy = GetScaleY(view.Zoom());
            int x = (int)((sx * markerVis_.X()) - (iconWidth_ / 2) - viewX);
            int y = (int)((sy * markerVis_.Y()) - (iconHeight_ / 2) - viewY);

            String name = markerVis_.Marker().Name();
            if(name == null) return;

            FontMetrics metrics = g.getFontMetrics(nameFont_);
            int nameH = metrics.getHeight();
            int nameW = metrics.stringWidth(name);

            // Deseneaza un dreptunghi in jurul textului.
            int alpha = (int)(255 * animation_.Opacity());
            g.setColor(new Color(189, 223, 147, alpha));
            g.fillRoundRect(x + iconWidth_ + 4, y,
                            nameW + 9, nameH + 2, 10, 10);

            g.setColor(new Color(90, 90, 90, alpha));
            g.drawRoundRect(x + iconWidth_ + 4, y,
                            nameW + 9, nameH + 2, 10, 10);

            // Afiseaza textul.
            g.setColor(new Color(0, 0, 0, alpha));
            g.setFont(nameFont_);
            g.drawString(name, x + iconWidth_ + 8,
                               y + (iconHeight_ / 2) + (nameH / 4));
        }

        public MarkerVisual MarkerVisual() { return markerVis_; }
        public NameAnimation Animation() { return animation_; }
        public void SetAnimation(NameAnimation value) { animation_ = value; }
        public Point Position() { return Point.Zero; }

        @Override
        public int hashCode() {
            return markerVis_.marker_.ID().hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if(obj == null) return false;
            if(getClass() != obj.getClass()) return false;
            
            final MarkerOverlay other = (MarkerOverlay) obj;
            if(markerVis_ != other.markerVis_ &&
              (markerVis_ == null || !markerVis_.equals(other.markerVis_))) {
                return false;
            }
            return true;
        }
    }


    // Animatie folosita pentru afisarea/ascunderea numelui.
    enum AnimationType {
        Show,
        Hide
    }

    // Animatie folosita la afisarea/ascunderea numelui unui marker.
    class NameAnimation extends AnimationBase {
        private MarkerVisual marker_;
        private AnimationType type_;
        private double start_;
        private double end_;
        private double value_;

        public NameAnimation(MarkerVisual marker, MarkerRenderer parent,
                             AnimationType type, double start, double end,
                             long duration, IInterpolation interpolation) {
            super(parent, duration, interpolation);
            marker_ = marker;
            type_ = type;
            start_ = start;
            end_ = end;
        }

        public AnimationType Type() { return type_; }
        public MarkerVisual Marker() { return marker_; }
        public double Opacity() { return value_; }

        @Override
        public void Update() {
            value_ = interpolation_.GetValue(start_, end_, Progress());
        }

        @Override
        public void Start() { StartWatch(); }
    }

    /*
     * Constante.
     */
    private static long ANIMATION_DURATION = 200;
    private static int NAME_SIZE = 14;

    /*
     * Membrii.
     */
    private IMapProvider provider_;
    private IRendererHost host_;
    private ILayer layer_;
    private IProjection projection_;
    private int zoomLevels_;
    private Region2D maxBounds_;
    private double opacity_;
    private PointTree<MarkerVisual> markers_;
    private Image icon_;
    private boolean visible_;
    private BasicCollection<MarkerVisual> visibleMarkers_;
    private int iconWidth_;
    private int iconHeight_;
    private double[] scaleX;
    private double[] scaleY;
    private AlphaComposite opacityComp_;
    private Font nameFont_;
    private MarkerVisual prevMarker;

    /*
     * Constructori.
     */
    public MarkerRenderer(ILayer layer, IRendererHost host) {
        host_ = host;
        layer_ = layer;
        provider_ = host.MapProvider();
        projection_ = provider_.Projection();
        zoomLevels_ = provider_.ZoomLevels();
        visible_ = true; // Initial vizibil.

        // Incarca markerele in arbore.
        maxBounds_ = provider_.MapBounds(zoomLevels_ - 1);
        markers_ = new PointTree<MarkerVisual>(maxBounds_.Width(), maxBounds_.Height());
        
        try {
            icon_ = provider_.LoadMarkerIcon(layer.ID());
        } catch (IOException ex) {
            Logger.getLogger(MarkerRenderer.class.getName()).log(Level.SEVERE, null, ex);
        }

        iconWidth_ = icon_.getWidth((ImageObserver)host);
        iconHeight_ = icon_.getHeight((ImageObserver)host);

        SetOpacity(1.0);
        visibleMarkers_ = new BasicCollection<MarkerVisual>();

        // Incarca marker-ele.
        Iterator<Marker> markerIt = provider_.GetMarkerIterator(layer.ID());
        while(markerIt.hasNext()) {
            Marker marker = markerIt.next();
            Point point = projection_.FromCoordinates(marker.Coordinates(),
                                                      zoomLevels_ - 1);
            MarkerVisual markerVis = new MarkerVisual(marker, point);
            markers_.Add(markerVis);
        }

        // Calculeaza factorii de scalare.
        ComputeScaleFactor();

        // Fontul folosit pentru nume.
        nameFont_ = new Font("Dialog", Font.BOLD, NAME_SIZE);
    }

    public boolean IsEditor() { return false; }
    public ILayer Layer() { return layer_; }
    public int ZIndex() { return 2; }
    public void SetZIndex(int value) {}
    public boolean HasPrefetcher() { return false; }
    public IPrefetcher Prefetcher() {
        throw new UnsupportedOperationException("Not supported.");
    }

    public boolean MouseDown(Point point, View view, Modifier modifier) {
        // Raporteaza daca un marker se afla sub cursor.
        IObjectInfo info = HitTest(point, view);
        if(info != null) {
            host_.SendAction(Action.ObjectSelected(info));
            return true;
        }

        return false;
    }

    public boolean MouseUp(Point point, View view, Modifier modifier) {
        return MouseDown(point, view, modifier);
    }
    
    public boolean MouseDragged(Point point, View view, Modifier modifier) { return false; }

    public boolean MouseMoved(Point point, View view, Modifier modifier) {
        MarkerVisual result = HitTestImpl(point, view);
        if(result != null) {
            if(result.equals(prevMarker) == false) {
                // Cursorul a ajuns peste un nou marker.
                if(prevMarker != null) {
                    HideMarkerName(result);
                }

                ShowMarkerName(result);
                prevMarker = result;
            }
        }
        else {
            if(prevMarker != null) {
                // Cursorul nu mai este peste marker.
                HideMarkerName(prevMarker);
                prevMarker = null;
            }
        }

        return false;
    }

    public boolean MouseCaptured() { return false; } // Nu poate captura mouse-ul.

    public double Opacity() { return opacity_; }
    public void SetOpacity(double value) {
        opacity_ = value;
        opacityComp_ = AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                                                 (float)opacity_);
    }

    public boolean Visible() { return visible_; }
    public void SetVisible(boolean value) { visible_ = value; }
    
    public IObjectInfo HitTest(Point point, View view) {
        // Gaseste cel mai apropiat marker. Daca distanta pana la
        // marker este de maxim dimensiunea imaginii => selectat.
        MarkerVisual markerVis = HitTestImpl(point, view);
        if(markerVis != null) {
            Node nearestNode = markerVis.Marker().NearestNode() != null ?
                               provider_.GetNode(markerVis.Marker().NearestNode()) :
                               null;
            Point temp = new Point(markerVis.X(), markerVis.Y());
            return new MarkerInfo(markerVis.Marker(), layer_, this, temp,
                                  projection_.ToCoordinates(temp, (int)view.Zoom()),
                                  nearestNode);
        }

        return null;
    }

    public void AddMarker(Marker marker) {
        Point point = projection_.FromCoordinates(marker.Coordinates(),
                                                      zoomLevels_ - 1);
        MarkerVisual markerVis = new MarkerVisual(marker, point);
        markers_.Add(markerVis);
        visibleMarkers_.Add(markerVis);
    }

    public void RemoveMarker(MarkerVisual markerVis) {
        markers_.Remove(markerVis);
    }

    public void MoveMarker(MarkerVisual markerVis, Point newPosition) {
        markers_.Remove(markerVis);
        markerVis.SetPosition(newPosition);
        markers_.Add(markerVis);
    }

    public MarkerVisual GetMarkerVisual(Marker marker) {
        // Se cauta doar in lista cu marker-e vizibile.
        for(MarkerVisual markerVis : visibleMarkers_.Objects()) {
            if(markerVis.Marker().equals(marker)) {
                return markerVis;
            }
        }

        // Nu trebuie sa se ajunga aici.
        throw new RuntimeException("Marker visual not found.");
    }

    public void Render(View view) {
        VolatileImage buffer = view.GetBuffer(this);
        Graphics2D g = buffer.createGraphics();
        g.setComposite(opacityComp_); // Seteaza opacitatea.
        
        // Activeaza antialiasing.
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                           RenderingHints.VALUE_ANTIALIAS_ON);

        // Obtine punctele din regiunea curenta.
        Region2D bounds = view.LastBounds();
        double inflateVal = 2 * Math.max(iconWidth_, iconHeight_);
        
        visibleMarkers_.Clear();
        bounds.Inflate(inflateVal, inflateVal);
        markers_.Intersect(bounds, visibleMarkers_);
        bounds.Inflate(-inflateVal, -inflateVal);

        // Deseneaza punctele gasite.
        Draw(g, view);
        g.dispose();
    }

    // Metoda apelata cand se termina o animatie asociata cu un nume.
    public void AnimationCompleted(AnimationBase animation) {
        NameAnimation nameAnim = (NameAnimation)animation;
        if(nameAnim.Type() == AnimationType.Hide) {
            // Textul nu mai trebuie afisat.
            host_.Overlay().RemoveOverlay(nameAnim.Marker());
        }
    }

    /*
     * Metode private.
     */
    // Calculeaza factorii de scalare pe baza dimensiuniilor maxime
    // ale hartii la fiecare nivel de zoom.
    private void ComputeScaleFactor() {
        scaleX = new double[zoomLevels_];
        scaleY = new double[zoomLevels_];

        double sx = 1.0;
        double sy = 1.0;
        scaleX[zoomLevels_ - 1] = sx;
        scaleY[zoomLevels_ - 1] = sy;

        for(int i = zoomLevels_ - 2; i >= 0; i--) {
            Region2D cntBounds = provider_.MapBounds(i);
            Region2D prevBounds = provider_.MapBounds(i + 1);
            sx *= cntBounds.Width() / prevBounds.Width();
            sy *= cntBounds.Height() / prevBounds.Height();
            scaleX[i] = sx;
            scaleY[i] = sy;
        }
    }

    // Obtine factorul de scalare pe axa X.
    private double GetScaleX(double zoom) {
        double scale = zoom - Math.floor(zoom);
        return scaleX[(int)zoom] +
               (scaleX[(int)Math.ceil(zoom)] - scaleX[(int)zoom]) * scale;
    }

    // Obtine factorul de scalare pe axa Y.
    private double GetScaleY(double zoom) {
        double scale = zoom - Math.floor(zoom);
        return scaleY[(int)zoom] +
               (scaleY[(int)Math.ceil(zoom)] - scaleY[(int)zoom]) * scale;
    }

    private MarkerVisual HitTestImpl(Point point, View view) {
        double sx = 1.0 / GetScaleX(view.Zoom());
        double sy = 1.0 / GetScaleY(view.Zoom());
        Point test = new Point(sx * (point.X() + view.Bounds().Left()),
                               sy * (point.Y() + view.Bounds().Top()));

        PointTree<MarkerVisual>.NearestInfo nearest = markers_.NearestPoint(test);
        if(nearest == null) return null; // Doar daca nu-i nici un marker in layer.

        if(nearest.Distance() < Math.min(sx * iconWidth_, sy * iconHeight_)) {
            return nearest.Value();
        }

        return null;
    }

    // Afiseaza numele marker-ului specificat.
    private void ShowMarkerName(MarkerVisual markerVis) {
        IOverlayHost host = host_.Overlay();
        double start = 0.0;
        double end = 1.0;

        if(host.HasOverlay(markerVis)) {
            // Daca o animatie este in desfasurare opreste-o
            // si ajusteaza valoarea de start.
            MarkerOverlay prevOverlay = (MarkerOverlay)host.GetOverlay(markerVis);
            start = prevOverlay.Animation().Opacity();
            host_.RemoveAnimation(prevOverlay.Animation());
            host_.Overlay().RemoveOverlay(markerVis);
        }

        // Adauga noua animatie.
        NameAnimation animation = new NameAnimation(markerVis, this, AnimationType.Show,
                                                    start, end, ANIMATION_DURATION,
                                                    new LinearInterpolation());

        MarkerOverlay overlay = new MarkerOverlay(markerVis, animation);
        host_.Overlay().AddOverlay(markerVis, overlay);
        host_.AddAnimation(animation);
    }

    // Ascunde numele marker-ului specificat.
    private void HideMarkerName(MarkerVisual markerVis) {
        IOverlayHost host = host_.Overlay();
        double start = 1.0;
        double end = 0.0;

        if(host.HasOverlay(markerVis)) {
            // Daca o animatie este in desfasurare opreste-o
            // si ajusteaza valoarea de start.
            MarkerOverlay prevOverlay = (MarkerOverlay)host.GetOverlay(markerVis);
            start = prevOverlay.Animation().Opacity();
            host_.Overlay().RemoveOverlay(markerVis);
            host_.RemoveAnimation(prevOverlay.Animation());
        }

        // Adauga noua animatie.
        NameAnimation animation = new NameAnimation(markerVis, this, AnimationType.Hide,
                                                    start, end, ANIMATION_DURATION,
                                                    new LinearInterpolation());

        MarkerOverlay overlay = new MarkerOverlay(markerVis, animation);
        host_.Overlay().AddOverlay(markerVis, overlay);
        host_.AddAnimation(animation);
    }

    // Afiseaza toate markerele din regiunea curenta.
    private void Draw(Graphics2D g, View view) {
        double viewX = view.Bounds().Left();
        double viewY = view.Bounds().Top();
        double sx = GetScaleX(view.Zoom());
        double sy = GetScaleY(view.Zoom());

        Iterator<MarkerVisual> markerIt = visibleMarkers_.Objects().iterator();
        while(markerIt.hasNext()) {
            MarkerVisual markerVis = markerIt.next();
            int x = (int)((sx * markerVis.X()) - (iconWidth_ / 2) - viewX);
            int y = (int)((sy * markerVis.Y()) - (iconHeight_ / 2) - viewY);

            g.setColor(new Color(0, 0, 0, 10));
            g.fillOval(x - 7, y - 7, (int)iconWidth_ + 14, (int)iconHeight_ + 14);
            g.drawImage(icon_, x, y, (ImageObserver)host_);
        }
    }
}